/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package
{
	import sweezy.swf.ITag;
	import sweezy.swf.SWF;
	import sweezy.swf.tag.Tag;

	public class Tester
	{

		private var _preview:Boolean;

		private var _tagMap:Object = {};

		private var _tagVersionMap:Object = {};

		private var _targets:Array;

		private var _versionMap:Object = {};

		private var _view:Main;

		public function Tester(targets:Array, view:Main)
		{
			_targets = targets;
			_view = view;
		}

		public function doTest():void
		{
			_view.setBlock(true);
			_view.print("テストを開始します。");

			for each (var name:String in Tag.getKnownTagNames())
			{
				_tagMap[name] = 0;
			}
			for (var i:int = 1; i <= 13; i++)
			{
				_versionMap[i] = 0;
			}
			for each (var code:uint in Tag.getKnownTagCodes())
			{
				_tagVersionMap[Tag.getTagNameByCode(code)] = Tag.getTagVersionByCode(code);
			}

			_view.callLater(doTest0);
		}

		public function doTestWithPreview():void
		{
			_preview = true;
			doTest();
		}

		private function complteTest():void
		{
			_view.setBlock(false);
			_view.print("テストが終了しました。");
			_view.print();

			var name:String;
			var count:uint;

			_view.print("テストしたタグ");
			for (name in _tagMap)
			{
				count = _tagMap[name];
				if (count > 0)
				{
					_view.print(name);
				}
			}
			_view.print();

			_view.print("テストしたバージョン");
			for (name in _versionMap)
			{
				count = _versionMap[name];
				if (count > 0)
				{
					_view.print(name);
				}
			}
			_view.print();

			_view.print("テストしていないタグ");
			for (name in _tagMap)
			{
				count = _tagMap[name];
				if (count === 0)
				{
					_view.print(name + "(" + _tagVersionMap[name] + ")");
				}
			}
			_view.print();

			_view.print("テストしていないバージョン");
			for (name in _versionMap)
			{
				count = _versionMap[name];
				if (count === 0)
				{
					_view.print(name);
				}
			}

			_view.update();
		}

		private function doTest0():void
		{
			if (_targets.length === 0)
			{
				complteTest();
				return;
			}

			var callback:Function = function(swf:SWF = null):void
			{
				if (swf !== null)
				{
					updateResult(swf);
				}
				_view.callLater(doTest0);
			};
			var task:TestTask = new TestTask(_targets.shift(), callback, _view, _preview);
			task.start();
		}

		private function updateResult(swf:SWF):void
		{
			for each (var tag:ITag in swf.tags)
			{
				if (tag.elementName in _tagMap)
				{
					_tagMap[tag.elementName]++;
				}
			}
			_versionMap[swf.header.version]++;
		}
	}
}

import flash.events.ErrorEvent;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.system.LoaderContext;
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
import flash.utils.getTimer;
import mx.utils.SHA256;
import sweezy.core.reflect.IProperty;
import sweezy.core.reflect.IType;
import sweezy.core.reflect.Reflection;
import sweezy.swf.ITag;
import sweezy.swf.SWF;
import sweezy.swf.abc.ABC;
import sweezy.swf.action.IAction;
import sweezy.swf.decoder.ABCDecoder;
import sweezy.swf.decoder.ActionDecoder;
import sweezy.swf.decoder.SWFDecoder;
import sweezy.swf.encoder.ABCEncoder;
import sweezy.swf.encoder.ActionEncoder;
import sweezy.swf.encoder.SWFEncoder;
import sweezy.swf.structure.ActionRecords;
import sweezy.swf.structure.ButtonCondAction;
import sweezy.swf.structure.ClipActionRecord;
import sweezy.swf.tag.DefineButton;
import sweezy.swf.tag.DefineButton2;
import sweezy.swf.tag.DoABC;
import sweezy.swf.tag.DoABC2;
import sweezy.swf.tag.DoAction;
import sweezy.swf.tag.DoInitAction;
import sweezy.swf.tag.PlaceObject2;
import sweezy.swf.tag.PlaceObject3;

class TestTask
{

	private var _callback:Function;

	private var _callback0:Function;

	private var _callback1:Function;

	private var _encodedBytes:ByteArray;

	private var _fileName:String;

	private var _firstDecodedSWF:SWF;

	private var _originalBytes:ByteArray;

	private var _preview:Boolean;

	private var _secondDecodedSWF:SWF;

	private var _target:TestTarget;

	private var _view:Main;

	public function TestTask(target:TestTarget, callback:Function, view:Main, preview:Boolean)
	{
		_target = target;
		if (preview)
		{
			_callback = testAction;
			_callback0 = callback;
			_callback1 = doPreview;
		}
		else
		{
			_callback = testAction;
			_callback1 = callback;
		}
		_view = view;
	}

	public function start():void
	{
		_view.print();
		status = "ファイルの取得を行います。";
		callLater(searchFile);
	}

	private function callLater(fn:Function):void
	{
		_view.callLater(_view.callLater, [fn]);
	}

	private function compareBytes():void
	{
		status = "バイナリの比較を行います。";
		callLater(compareBytes0);
	}

	private function compareBytes0():void
	{
		var original:ByteArray = uncompress(_originalBytes);
		var encoded:ByteArray = uncompress(_encodedBytes);
		if (original.length !== encoded.length)
		{
			status = "バイナリのサイズが違います。(original=" + original.length + ", encoded=" + encoded.length + ")";
			callLater(redecode);
			return;
		}

		for (var i:uint = 0, len:uint = original.length; i < len; i++)
		{
			if (original[i] !== encoded[i])
			{
				status = "バイナリに相違があります。(position=" + i + ")";
				for (var j:int = Math.max(0, i - 50), len2:uint = j + 100; j < len2; j++)
				{
					_view.print(j, original[j], encoded[j]);
				}
				callLater(redecode);
				return;
			}
		}

		status = "バイナリが一致しました。";
		result = "ok";
		_callback(_firstDecodedSWF);
	}

	private function compareStructure():void
	{
		status = "構造の比較を行います。";
		callLater(compareStructure0);
	}

	private function compareStructure0():void
	{
		var tags1:Array = _firstDecodedSWF.tags;
		var tags2:Array = _secondDecodedSWF.tags;

		if (tags1.length !== tags2.length)
		{
			status = "タグの数が一致しません。(first=" + tags1.length + ", second=" + tags2.length + ")";
			result = "ng";
			_callback(_firstDecodedSWF);
			return;
		}

		for (var i:int = 0, len:uint = tags1.length; i < len; i++)
		{
			var tag1:ITag = tags1[i];
			var tag2:ITag = tags2[i];
			if (tag1.elementName !== tag2.elementName || tag1.length !== tag2.length)
			{
				_view.print("一致しないタグ", "index=", i, "first=", tag1.elementName, tag1.position, tag1.length, "second=", tag2.elementName, tag2.position, tag2.length);
			}
		}

		if (!compareStructureStrict())
		{
			status = "厳密チェックで一致しませんでした。";
			result = "ng";
			_callback(_firstDecodedSWF);
			return;
		}

		status = "厳密チェックで一致しました。";
		result = "ok";
		_callback(_firstDecodedSWF);
	}

	private function compareStructureStrict():Boolean
	{
		var comparator:Comparator = new Comparator();
		comparator.compare(_firstDecodedSWF, _secondDecodedSWF);
		if (comparator.result)
		{
			return true;
		}

		_view.print(comparator.reason);
		return false;
	}

	private function decodeFirst():void
	{
		status = "デコードを行います。";
		callLater(decodeFirst0);
	}

	private function decodeFirst0():void
	{
		try
		{
			var decoder:SWFDecoder = new SWFDecoder();
			_firstDecodedSWF = decoder.decode(_originalBytes);
			if (decoder.hasErrors)
			{
				_view.print(decoder.errors.join("\n"));
			}
			callLater(encode);
		}
		catch (e:Error)
		{
			status = e.message;
			result = "error";
			_callback();
		}
	}

	private function doPreview(swf:SWF = null):void
	{
		if (_originalBytes !== null && _encodedBytes !== null)
		{
			var window:PreviewWindow = new PreviewWindow();
			window.open();
			window.validateNow();

			var context:LoaderContext = new LoaderContext();
			context.allowLoadBytesCodeExecution = true;

			window.originalImage.loaderContext = context;
			window.originalImage.source = _originalBytes;
			window.encodedImage.loaderContext = context;
			window.encodedImage.source = _encodedBytes;
		}

		_callback0(swf);
	}

	private function downloadFile():void
	{
		var loader:URLLoader = new URLLoader();
		loader.dataFormat = URLLoaderDataFormat.BINARY;
		loader.addEventListener(Event.COMPLETE, downloadHandler);
		loader.addEventListener(IOErrorEvent.IO_ERROR, downloadHandler);
		loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, downloadHandler);
		loader.load(new URLRequest(_target.url));
	}

	private function downloadHandler(e:Event):void
	{
		var loader:URLLoader = e.currentTarget as URLLoader;
		loader.removeEventListener(Event.COMPLETE, downloadHandler);
		loader.removeEventListener(IOErrorEvent.IO_ERROR, downloadHandler);
		loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, downloadHandler);

		if (e is ErrorEvent)
		{
			result = "error";
			status = ErrorEvent(e).text;
			_callback();
			return;
		}

		var bytes:ByteArray = loader.data;
		var stream:FileStream = new FileStream();
		var file:File = File.applicationStorageDirectory.resolvePath(_fileName);
		try
		{
			stream.open(file, FileMode.WRITE);
			stream.writeBytes(bytes);
			_originalBytes = bytes;
			callLater(decodeFirst);

			return;
		}
		catch (e:Error)
		{
			result = e.message;
			status = "error";
			_callback();
		}
		finally
		{
			try
			{
				stream.close();
			}
			catch (e:Error)
			{
			}
		}
	}

	private function encode():void
	{
		status = "エンコードを行います。";
		callLater(encode0);
	}

	private function encode0():void
	{
		var encoder:SWFEncoder = new SWFEncoder();
		try
		{
			_encodedBytes = encoder.encode(_firstDecodedSWF);
			callLater(compareBytes);
		}
		catch (e:Error)
		{
			status = e.message;
			result = "error";
			_callback();
		}
	}

	private function redecode():void
	{
		status = "再デコードを行います。";
		callLater(redecode0);
	}

	private function redecode0():void
	{
		try
		{
			var decoder:SWFDecoder = new SWFDecoder();
			_secondDecodedSWF = decoder.decode(_encodedBytes);
			if (decoder.hasErrors)
			{
				_view.print(decoder.errors.join("\n"));
			}
			callLater(compareStructure);
		}
		catch (e:Error)
		{
			status = e.message;
			result = "error";
			_callback();
		}
	}

	private function set result(value:String):void
	{
		_target.result = value;
		_view.update();
	}

	private function searchFile():void
	{
		var fileNameBytes:ByteArray = new ByteArray();
		fileNameBytes.writeUTFBytes(_target.url);
		fileNameBytes.position = 0;

		var fileName:String = SHA256.computeDigest(fileNameBytes) + ".swf";
		var file:File = File.applicationStorageDirectory.resolvePath(fileName);
		if (file.exists)
		{
			var bytes:ByteArray = new ByteArray();

			var stream:FileStream = new FileStream();
			try
			{
				stream.open(file, FileMode.READ);
				stream.readBytes(bytes);
				_originalBytes = bytes;
				callLater(decodeFirst);

				return;
			}
			catch (e:Error)
			{
			}
			finally
			{
				try
				{
					stream.close();
				}
				catch (e:Error)
				{
				}
			}
		}

		status = "ファイルのダウンロードを行います。";
		_fileName = fileName;
		callLater(downloadFile);
	}

	private function set status(value:String):void
	{
		_view.print("[" + _target.id + "]" + value);
		_target.status = value;
		_view.update();
	}

	private function testABC(bytes:ByteArray):Boolean
	{
		_view.print("ABCのテストを行います。");
		var abc:ABC;
		try
		{
			abc = new ABCDecoder().decode(bytes);
		}
		catch (e:Error)
		{
			result = "error";
			status = e.message;
			return false;
		}

		var bytes2:ByteArray;
		try
		{
			bytes2 = new ABCEncoder().encode(abc);
		}
		catch (e:Error)
		{
			result = "error";
			status = e.message;
			return false;
		}

		if (bytes.length !== bytes2.length)
		{
			result = "ng";
			status = "ABCのサイズが一致しません(original=" + bytes.length + ", encoded=" + bytes2.length + ")";
			return false;
		}

		for (var i:uint = 0, len:uint = bytes.length; i < len; i++)
		{
			if (bytes[i] !== bytes2[i])
			{
				result = "ng";
				status = "ABCのバイナリが一致しません。(position=" + i + ")";
				return false;
			}
		}

		_view.print("ABCのバイナリが一致しました。");
		return true;
	}

	private function testAction(swf:SWF = null):void
	{
		if (swf !== null)
		{
			for each (var tag:ITag in swf.tags)
			{
				if (tag is DoABC)
				{
					if (!testABC(DoABC(tag).abcData.data))
					{
						break;
					}
				}
				else if (tag is DoABC2)
				{
					if (!testABC(DoABC2(tag).abcData.data))
					{
						break;
					}
				}
			}
		}

		_callback1(swf);
	}

	private function uncompress(bytes:ByteArray):ByteArray
	{
		if (bytes[0] !== "C".charCodeAt(0))
		{
			return bytes;
		}

		var tmpBytes:ByteArray = new ByteArray();
		tmpBytes.writeBytes(bytes, 8);
		tmpBytes.uncompress();

		var newBytes:ByteArray = new ByteArray();
		for (var i:int = 0; i < 8; i++)
		{
			newBytes[i] = bytes[i];
		}
		newBytes.position = 8;
		newBytes.writeBytes(tmpBytes);

		return newBytes;
	}
}

class Comparator
{

	public var reason:String;

	public var result:Boolean;

	private var _reflectionCache:Dictionary;

	public function Comparator()
	{
		reason = "";
		_reflectionCache = new Dictionary();
	}

	public function compare(value1:Object, value2:Object):void
	{
		result = compare0(value1, value2);
	}

	private function compare0(value1:Object, value2:Object):Boolean
	{
		if (value1 === value2)
		{
			return true;
		}
		if (value1 === null || value2 === null)
		{
			trace0(false, "nullとnullではない値です。", value1, value2);
			return false;
		}

		/**
		var typeName1:String = getQualifiedClassName(value1);
		var typeName2:String = getQualifiedClassName(value2);
		if (typeName1 !== typeName2)
		{
			trace0(false, "型が一致しません。", value1, value2);
			return false;
		}
		*/

		var i:uint, len:uint;
		if (value1 is ByteArray)
		{
			var bytes1:ByteArray = value1 as ByteArray;
			var bytes2:ByteArray = value2 as ByteArray;
			if (bytes1.length !== bytes2.length)
			{
				trace0(false, "バイトサイズが一致しません。", bytes1.length, bytes2.length);
				return false;
			}

			len = bytes1.length;
			for (i = 0; i < len; i++)
			{
				if (bytes1[i] !== bytes2[i])
				{
					trace0(false, "バイトが一致しません。", bytes1.length, bytes2.length);
					return false;
				}
			}

			return true;
		}
		if (value1 is Array)
		{
			var array1:Array = value1 as Array;
			var array2:Array = value2 as Array;
			if (array1.length !== array2.length)
			{
				trace0(false, "配列の要素数が一致しません。", array1.length, array2.length);
				return false;
			}

			len = array1.length;
			for (i = 0; i < len; i++)
			{
				if (!compare0(array1[i], array2[i]))
				{
					return false;
				}
			}

			return true;
		}

		var type:IType = Reflection.forInstance(value1);
		var properties:Array = _reflectionCache[type];
		if (properties === null)
		{
			_reflectionCache[type] = properties = [];
			for each (var property:IProperty in type.properties)
			{
				if (!property.isVariable)
				{
					continue;
				}
				if (property.isStatic)
				{
					continue;
				}
				if (property.isConst)
				{
					continue;
				}
				if(property.name.toLocaleLowerCase().indexOf("minbit") > -1){
					continue;
				}

				properties[properties.length] = property.name;
			}
		}

		for each (var name:String in properties)
		{
			var pv1:Object = value1[name];
			var pv2:Object = value2[name];
			if (pv1 === pv2)
			{
				continue;
			}

			if (!compare0(pv1, pv2))
			{
				trace0("プロパティが一致しません。", type.className, name);
				return false;
			}
		}

		return true;
	}

	private function trace0(... args):void
	{
		reason += args.join(" ") + "\n";
	}
}
